﻿/// 依赖关系：
///  fbx 模型文件依赖的动画 fbx 模型文件会被自动打包到 asset bundle 中。具体是由 Animation 组件添加的其他 Animation 资源。
///  材质文件依赖的纹理资源会被自动打包到 asset bundle 中。
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Object = UnityEngine.Object;

static class UnityHelper
{
    public static GameObject ReplacePrefab(GameObject go, string bundlePath, string name)
    {
        var assetPath = string.Format("{0}/{1}.prefab", bundlePath, name);
        var prefab = PrefabUtility.CreateEmptyPrefab(assetPath);
        prefab = PrefabUtility.ReplacePrefab(go, prefab);
        Object.DestroyImmediate(go);
        go = prefab as GameObject;
        return go;
    }

    // 获取指定目录下指定类型的对象。
    public static List<T> CollectAll<T>(string path, string pattern = null) where T : Object
    {
        var list = new List<T>();
        string[] files;
        if (string.IsNullOrEmpty(pattern))
            files = Directory.GetFiles(path);
        else
            files = Directory.GetFiles(path, pattern);

        foreach (var f in files)
        {
            var asset = AssetDatabase.LoadAssetAtPath<T>(f);
            if (asset != null)
                list.Add(asset);
        }
        return list;
    } 
}

public class DressingCreateABEditor : MonoBehaviour
{
    static string bundlePath = string.Format("Assets/Assetbundles");

    [MenuItem("Packager/Create Asset Bundle")]
    static void CreateAssetBundle()
    {
        if (!Directory.Exists(bundlePath))
            Directory.CreateDirectory(bundlePath);

        // 被打包的模型文件路径
        var modelPath = "Assets/Resources/Models";
        var filePaths = Directory.GetFiles(modelPath, "*.fbx", SearchOption.AllDirectories);
        var goFBXObjs = new List<GameObject>();
        var partBundleNames = new List<string>();
        foreach (var f in filePaths)
        {
            if (f.Contains("@")) // 不处理动画模型
                continue;
            var assetPath = f.Replace("\\", "/");
            var goFBX = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
            var goName = goFBX.name.ToLower();

            // step1
            CreateCharacterBaseBundle(goFBX, goName);
            // step2
            partBundleNames.AddRange(CreateCharacterPartBundle(goFBX, goName));

            goFBXObjs.Add(goFBX);
        }

        if (goFBXObjs.Count == 0)
            throw new Exception(string.Format("no fbx model file at path:{0}", modelPath));
        
        // step3
        CreateCharacterElementBundle(goFBXObjs, partBundleNames);
        Debug.Log("build all dressing room asset bundles successfully");
    }

    // 打包角色的基本信息，包括 Animation 组件，骨骼信息。
    // fbxname_characterbase.assetbundle
    static void CreateCharacterBaseBundle(GameObject goFBX, string goName)
    {
        var go = Object.Instantiate<GameObject>(goFBX); // 被替换销毁
        var assetName = "characterbase";
        var bundleName = string.Format("{0}_{1}.assetbundle", goName, assetName);

        // 检查 bundle 是否已经存在
        var checkPath = string.Format("{0}/{1}", bundlePath, bundleName);
        if (File.Exists(checkPath))
        {
            File.Delete(checkPath);
        }

        // 设置 Animation 组件属性
        foreach (var ani in go.GetComponentsInChildren<Animation>())
        {
            ani.cullingType = AnimationCullingType.AlwaysAnimate;
        }

        // 删除渲染用到的 GameObject 仅保留骨骼信息
        foreach (var smr in go.GetComponentsInChildren<SkinnedMeshRenderer>())
        {
            Object.DestroyImmediate(smr.gameObject);
        }
        go.AddComponent<SkinnedMeshRenderer>(); 

        var assetPrefab = UnityHelper.ReplacePrefab(go, bundlePath, assetName);
        var assetPath = AssetDatabase.GetAssetPath(assetPrefab);

        var buildMap = new AssetBundleBuild[1];
        buildMap[0].assetBundleName = bundleName;
        buildMap[0].assetNames = new string[] { assetPath };
        BuildPipeline.BuildAssetBundles(bundlePath, buildMap, BuildAssetBundleOptions.StrictMode, BuildTarget.StandaloneWindows);

        AssetDatabase.DeleteAsset(assetPath); // 销毁 prefab
    }

    static string ModelMaterialPath(GameObject go)
    {
        var assetPath = AssetDatabase.GetAssetPath(go);
        var path = string.Format("{0}/Per Texture Materials", assetPath.Substring(0, assetPath.LastIndexOf('/')));
        if (!Directory.Exists(path))
        {
            throw new Exception(string.Format("material path:%s not exists", path));
        }
        return path;
    }

    // 打包角色可更换的肢体部位，包括了肢体部位对应的换装材质和骨骼信息。
    // 模型各肢体部位使用的材质资源命名格式是 fbxname_part-number_stype.mat 其中 style 就是不同的显示风格。
    // fbxname_part-number.assetbundle or fbxname_part.assetbundle
    static List<string> CreateCharacterPartBundle(GameObject goFBX, string goName)
    {
        var materialDir = ModelMaterialPath(goFBX);
        var materials = UnityHelper.CollectAll<Material>(materialDir, "*.mat");
        if (materials.Count == 0)
            throw new Exception(string.Format("no material at path:{0}", materialDir));

        var partBundleNames = new List<string>();

        // 遍历渲染组件，关联其对应的材质列表和骨骼信息
        foreach (var smr in goFBX.GetComponentsInChildren<SkinnedMeshRenderer>(true))
        {
            var includeObjs = new List<Object>();
            var partName = smr.name.ToLower();

            // 使用 InstantiatePrefab 可以保持 GameObject 的名称，
            // 而如果使用 Object.Instantiate 会带有 Clone 。
            var goRender = PrefabUtility.InstantiatePrefab(smr.gameObject) as GameObject;
            // 使用 InstantiatePrefab 也会包括 parent 所以需要去除。
            var goParent = goRender.transform.parent.gameObject;
            goRender.transform.SetParent(null);
            Object.DestroyImmediate(goParent);
            var renderPrefab = UnityHelper.ReplacePrefab(goRender, bundlePath, "renderobject");
            includeObjs.Add(renderPrefab);

            // 根据名字关联该肢体部位对应的材质球
            foreach (var m in materials)
            {
                if (m.name.Contains(partName))
                    includeObjs.Add(m);
            }

            // 骨骼信息
            var boneNames = new List<string>();
            foreach(var b in smr.bones)
                boneNames.Add(b.name);

            var boneNameAssetPath = string.Format("{0}/bonenames.asset", bundlePath);
            var boneHolder = ScriptableObject.CreateInstance<StringHolder>();
            boneHolder.content = boneNames.ToArray();
            AssetDatabase.CreateAsset(boneHolder, boneNameAssetPath);
            var boneAsset = AssetDatabase.LoadAssetAtPath<StringHolder>(boneNameAssetPath);
            includeObjs.Add(boneAsset);

            var includePath = new List<string>();
            foreach (var o in includeObjs)
                includePath.Add(AssetDatabase.GetAssetPath(o));

            var bundleName = string.Format("{0}_{1}.assetbundle", goName, partName);
            var buildMap = new AssetBundleBuild[1];
            buildMap[0].assetNames = includePath.ToArray();
            buildMap[0].assetBundleName = bundleName;
            BuildPipeline.BuildAssetBundles(bundlePath, buildMap, BuildAssetBundleOptions.StrictMode, BuildTarget.StandaloneWindows);

            AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(renderPrefab));
            AssetDatabase.DeleteAsset(boneNameAssetPath);
            partBundleNames.Add(bundleName);
        }

        return partBundleNames;
    }

    // 材质资源被打包进了肢体部位 asset bundle 中，
    // 将生成的各肢体部位 asset bundle 与对应的材质资源名称建立映射。
    // 产生一个 asset bundle 包含所有的模型中的映射关系。
    // characterelements.assetbundle
    static void CreateCharacterElementBundle(List<GameObject> goFBXObjs, List<string> partBundleNames)
    {
        var materialNames = new List<string>();
        var pathSeparetors = new char[] { '\\', '/' };
        foreach (var go in goFBXObjs)
        {
            var path = ModelMaterialPath(go);
            var filesPath = Directory.GetFiles(path, "*.mat");
            foreach (var f in filesPath)
            {
                var start = f.LastIndexOfAny(pathSeparetors) + 1;
                if (start == 0)
                    throw new Exception(string.Format("invalid material path:{0}", path));
                var n = f.Substring(start); // include .mat
                materialNames.Add(n);
            }
        }

        var elems = new List<CharacterElement>();
        foreach (var mname in materialNames)
        {
            var found = false;
            foreach (var pbname in partBundleNames)
            {
                if (mname.Contains(pbname.Replace(".assetbundle", "")))
                {
                    found = true;
                    elems.Add(new CharacterElement(mname, pbname));
                    break;
                }
            }

            if (!found)
                throw new Exception(string.Format("no part bundle match with material:{0}", mname));
        }

        var elemHolder = ScriptableObject.CreateInstance<CharacterElementHolder>();
        elemHolder.content = elems.ToArray();

        var pureName = "characterelements";
        var assetPath = string.Format("{0}/{1}.asset", bundlePath, pureName);
        AssetDatabase.CreateAsset(elemHolder, assetPath);

        var bundleName = string.Format("{0}.assetbundle", pureName);
        var buildMap = new AssetBundleBuild[1];
        buildMap[0].assetBundleName = bundleName;
        buildMap[0].assetNames = new string[] { assetPath };
        BuildPipeline.BuildAssetBundles(bundlePath, buildMap, BuildAssetBundleOptions.StrictMode, BuildTarget.StandaloneWindows);

        AssetDatabase.DeleteAsset(assetPath);
    }
}
